Concepte fundamentale de limbaje de programare

5. Domenii majore de limbaj

5.1 Matrici, colecții și LINQ

C# și .NET oferă multe tipuri de colecții diferite. Matricele au sintaxă definită de limbaj. Tipurile de colecții generice sunt listate în namespace-ul System.Collections.Generic. Colecțiile specializate includ System.Span<T> pentru accesarea memoriei continue pe cadrul stivei și System.Memory<T> pentru accesarea memoriei continue pe heap-ul gestionat. Toate colecțiile, inclusiv matricele, Span<T> și Memory<T> au un principiu unificator pentru iterare. Utilizăm interfața System.Collections.Generic.IEnumerable<T>. Acest principiu unificator înseamnă că oricare dintre tipurile de colecție poate fi utilizat cu interogări LINQ sau alți algoritmi. Scrieți metode folosind IEnumerable<T>, iar acei algoritmi funcționează cu orice colecție.

5.2 Matrici

O matrice este o structură de date care conține un număr de variabile care sunt accesate prin indici prestabiliți. Variabilele conținute într-o matrice, numite și elementele matricei, sunt toate de același tip. Acest tip se numește tipul de element al matricei.

Tipurile de matrice sunt tipuri de referință, iar declararea unei variabile de matrice pur și simplu pune deoparte spațiu pentru o referință la o instanță de matrice. Instanțele reale ale matricei sunt create dinamic în timpul rulării utilizând operatorul new. Operatorul new specifică lungimea noii instanțe de matrice, care este apoi fixată pe durata de viață a instanței. Indicii elementelor unui tablou variază de la 0 la Lungime - 1. Operatorul new inițializează automat elementele unui tablou la valoarea lor implicită, care, de exemplu, este zero pentru toate tipurile numerice și nulă pentru toate tipurile de referință. Următorul exemplu creează o matrice de elemente int, inițializează matricea și tipărește conținutul matricei.

int[] a = new int[10];
for (int i = 0; i < a.Length; i++)
{
    a[i] = i * i;
}
for (int i = 0; i < a.Length; i++)
{
    Console.WriteLine($"a[{i}] = {a[i]}");
}
Acest exemplu creează și operează pe o matrice unidimensională. C# acceptă și matrice multidimensionale. Numărul de dimensiuni ale unui tip de matrice, cunoscut și ca rangul tipului de matrice, este unul plus numărul de virgule dintre parantezele pătrate ale tipului de matrice. Următorul exemplu alocă o matrice unidimensională, bidimensională și, respectiv, tridimensională.
int[] a1 = new int[10];
int[,] a2 = new int[10, 5];
int[,,] a3 = new int[10, 5, 2];

Matricea a1 conține 10 elemente, matricea a2 conține 50 (10 × 5) elemente și matricea a3 conține 100 (10 × 5 × 2) elemente. Tipul de element al unei matrice poate fi orice tip, inclusiv un tip de matrice. O matrice cu elemente de tip matrice este uneori numită matrice zimțată deoarece lungimile matricelor de elemente nu trebuie să fie toate aceleași. Următorul exemplu alocă o matrice de matrice de int:

int[][] a = new int[3][];
a[0] = new int[10];
a[1] = new int[5];
a[2] = new int[20];

Prima linie creează o matrice cu trei elemente, fiecare de tip int[] și fiecare cu o valoare inițială nulă. Următoarele linii inițializează apoi cele trei elemente cu referințe la instanțe de matrice individuale de lungimi diferite.

Operatorul new permite ca valorile inițiale ale elementelor de matrice să fie specificate folosind un inițializator de matrice, care este o listă de expresii scrise între delimitatorii { and }. Următorul exemplu alocă și inițializează un int[] cu trei elemente.

int[] a = new int[] { 1, 2, 3 };

Lungimea matricei este dedusă din numărul de expresii cuprinse între { and }. Inițializarea matricei poate fi scurtată și mai mult, astfel încât tipul matricei nu trebuie să fie reformulat.

int[] a = { 1, 2, 3 };
Ambele exemple anterioare sunt echivalente cu următorul cod:
int[] t = new int[3];
t[0] = 1;
t[1] = 2;
t[2] = 3;
int[] a = t;
Instrucțiunea foreach poate fi folosită pentru a enumera elementele oricărei colecții. Următorul cod enumeră matricea din exemplul precedent:
foreach (int item in a)
{
    Console.WriteLine(item);
}
Instrucțiunea foreach folosește interfața IEnumerable, astfel încât poate funcționa cu orice colecție.

5.3 Interpolarea de șiruri

Interpolarea șirurilor în C# vă permite să formatați șiruri prin definirea expresiilor ale căror rezultate sunt plasate într-un format de tip string. De exemplu, următorul exemplu tipărește temperatura într-o anumită zi dintr-un set de date meteo:

Console.WriteLine($"The low and high temperature on {weatherData.Date:MM-DD-YYYY}");
Console.WriteLine($"    was {weatherData.LowTemp} and {weatherData.HighTemp}.");

// Output (similar to):
// The low and high temperature on 08-11-2020
//     was 5 and 30.

Un șir interpolat este declarat folosind tokenul $. Interpolarea șirurilor evaluează expresiile dintre { and }, apoi convertește rezultatul într-un șir și înlocuiește textul dintre paranteze cu rezultatul șirului expresiei. : din prima expresie, {weatherData.Date:MM-DD-AAAA} specifică formatul de string. În exemplul precedent, se specifică că data trebuie tipărită în format „LL-ZZ-AAAA”.

5.4 Potrivire de şablon

Limbajul C# oferă expresii de potrivire a şabloanelor pentru a interoga starea unui obiect și pentru a executa cod pe baza acelei stări. Puteți inspecta tipurile și valorile proprietăților și câmpurilor pentru a determina ce acțiune să luați. Puteți inspecta și elementele unei liste sau ale unei matrice. Expresia switch este expresia principală pentru potrivirea şablonului.

5.5 Delegați și expresii lambda

Un tip delegat reprezintă referințe la metode cu o anumită listă de parametri și un tip de returnare. Delegații fac posibilă tratarea metodelor ca entități care pot fi atribuite variabilelor și transmise ca parametri. Delegații sunt similari conceptului de pointeri de funcție găsit în alte limbi. Spre deosebire de pointerii de funcție, delegații sunt orientați pe obiecte și sunt de tipul type-safe.

Următorul exemplu declară și utilizează un tip de delegat numit Function.

delegate double Function(double x);

class Multiplier
{
    double _factor;

    public Multiplier(double factor) => _factor = factor;

    public double Multiply(double x) => x * _factor;
}

class DelegateExample
{
    static double[] Apply(double[] a, Function f)
    {
        var result = new double[a.Length];
        for (int i = 0; i < a.Length; i++) result[i] = f(a[i]);
        return result;
    }

    public static void Main()
    {
        double[] a = { 0.0, 0.5, 1.0 };
        double[] squares = Apply(a, (x) => x * x);
        double[] sines = Apply(a, Math.Sin);
        Multiplier m = new(2.0);
        double[] doubles = Apply(a, m.Multiply);
    }
}

O instanță de tip delegat Function poate face referire la orice metodă care ia un argument dublu și returnează o valoare dublă. Metoda Apply aplică o funcție dată elementelor unui double[], returnând un double[] cu rezultatele. În metoda Main, Apply este folosit pentru a aplica trei funcții diferite unui double[].

Un delegat poate face referire fie la o metodă statică (cum ar fi Square sau Math.Sin în exemplul anterior), fie la o metodă de instanță (cum ar fi m.Multiply în exemplul anterior). Un delegat care face referire la o metodă de instanță face referire și la un anumit obiect, iar atunci când metoda de instanță este invocată prin delegat, acel obiect devine acesta în invocare.

Delegații pot fi, de asemenea, creați folosind funcții anonime sau expresii lambda, care sunt „metode inline” care sunt create atunci când sunt declarate. Funcțiile anonime pot vedea variabilele locale ale metodelor din jur. Următorul exemplu nu creează o clasă:

double[] doubles = Apply(a, (double x) => x * 2.0);

Un delegat nu știe sau nu îi pasă de clasa metodei la care face referire. Metoda la care se face referire trebuie să aibă aceiași parametri și aceiași tip de returnare ca și delegatul.

5.6 async / await

C# acceptă programe asincrone cu două cuvinte cheie: async și await. Adăugăm modificatorul async la o declarație de metodă pentru a declara că metoda este asincronă. Operatorul await îi spune compilatorului să aștepte asincron pentru finalizarea unui rezultat. Controlul este returnat apelantului, iar metoda returnează o structură care gestionează starea lucrării asincrone. Structura este de obicei un System.Threading.Tasks.Task<TResult>, dar poate fi orice tip care acceptă modelul awaiter. Aceste caracteristici vă permit să scrieți cod care se citește ca omologul său sincron, dar care se execută asincron. De exemplu, următorul cod descarcă pagina de pornire pentru Microsoft docs:

public async Task<int> RetrieveDocsHomePage()
{
    var client = new HttpClient();
    byte[] content = await client.GetByteArrayAsync("https://docs.microsoft.com/");

    Console.WriteLine($"{nameof(RetrieveDocsHomePage)}: Finished downloading.");
    return content.Length;
}

Acest scurt exemplu arată caracteristicile majore pentru programarea asincronă:

Declarația metodei include modificatorul async. Corpul metodei așteaptă returnarea metodei GetByteArrayAsync. Tipul specificat în instrucțiunea return se potrivește cu argumentul tip din declarația Task<T> pentru metodă. (O metodă care returnează un Task ar folosi instrucțiuni return fără niciun argument).

5.7 Atribute

Tipurile, membrii și alte entități dintr-un program C# acceptă modificatori care controlează anumite aspecte ale comportamentului lor. De exemplu, accesibilitatea unei metode este controlată folosind modificatorii publici, protejați, interni și privați. C# generalizează această capacitate astfel încât tipurile de informații declarative definite de utilizator să poată fi atașate la entitățile programului și preluate în timpul execuției. Programele specifică aceste informații declarative prin definirea și utilizarea atributelor.

Următorul exemplu declară un atribut HelpAttribute care poate fi plasat pe entitățile programului pentru a furniza link-uri către documentația asociată acestora.

public class HelpAttribute : Attribute
{
    string _url;
    string _topic;

    public HelpAttribute(string url) => _url = url;

    public string Url => _url;

    public string Topic
    {
        get => _topic;
        set => _topic = value;
    }
}

Toate clasele de atribute derivă din clasa de bază Attribute furnizată de biblioteca .NET. Atributele pot fi aplicate dând numele lor, împreună cu orice argument, între paranteze drepte chiar înainte de declarația asociată. Dacă numele unui atribut se termină în Atributte, acea parte a numelui poate fi omisă atunci când se face referire la atribut. De exemplu, HelpAttribute poate fi folosit după cum urmează.

[Help("https://docs.microsoft.com/dotnet/csharp/tour-of-csharp/features")]
public class Widget
{
    [Help("https://docs.microsoft.com/dotnet/csharp/tour-of-csharp/features",
    Topic = "Display")]
    public void Display(string text) { }
}

Acest exemplu atașează un HelpAttribute la clasa Widget. Se adaugă un alt HelpAttribute la metoda Display din clasă. Constructorii publici ai unei clase de atribute controlează informațiile care trebuie furnizate atunci când atributul este atașat la o entitate de program. Informații suplimentare pot fi furnizate prin referirea la proprietățile publice de citire-scriere ale clasei de atribute (cum ar fi referința la proprietatea anterioară Topic).

Metadatele definite de atribute pot fi citite și manipulate în timpul execuției folosind reflectarea. Când un anumit atribut este solicitat folosind această tehnică, constructorul pentru clasa de atribut este invocat cu informațiile furnizate în sursa programului. Instanța de atribut rezultată este returnată. Dacă au fost furnizate informații suplimentare prin proprietăți, acele proprietăți sunt setate la valorile date înainte ca instanța atributului să fie returnată.

Următorul exemplu de cod demonstrează cum să obțineți instanțele HelpAttribute asociate clasei Widget și metodei sale Display.

Type widgetType = typeof(Widget);

object[] widgetClassAttributes = widgetType.GetCustomAttributes(typeof(HelpAttribute), false);

if (widgetClassAttributes.Length > 0)
{
    HelpAttribute attr = (HelpAttribute)widgetClassAttributes[0];
    Console.WriteLine($"Widget class help URL : {attr.Url} - Related topic : {attr.Topic}");
}

System.Reflection.MethodInfo displayMethod = widgetType.GetMethod(nameof(Widget.Display));

object[] displayMethodAttributes = displayMethod.GetCustomAttributes(typeof(HelpAttribute), false);

if (displayMethodAttributes.Length > 0)
{
    HelpAttribute attr = (HelpAttribute)displayMethodAttributes[0];
    Console.WriteLine($"Display method help URL : {attr.Url} - Related topic : {attr.Topic}");
}

5.8 Sarcini

TODO

Bibliografie

[1] Microsoft Corporation. C# Documentation, https://docs.microsoft.com/en-us/dotnet/csharp/, 2022.